/*	--- 定制Bean --- 


#	-- Scope  n. 能力,机会,范围

对于【Spring容器】来说，当我们把【一个Bean】标记为【@Component】后，它就会自动为我们【创建一个单例】（Singleton）

即【容器初始化时]创建[Bean】，【容器关闭前】销毁【Bean】。
在【容器运行期间】，我们调用【getBean(Class)】获取到的【Bean】总是【同一个实例】。

还有一种【Bean】，我们【每次调用getBean(Class)】，【容器】都返回【一个新的实例】，这种Bean称为【Prototype】（原型）

【Prototype的Bean的生命周期】显然和【Singleton不同】。

声明一个【Prototype的Bean】时，需要添加一个【额外的@Scope注解】： */
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype")
public class MailSession {
	//...
}


/*====================================================

#	-- 注入List

有些时候，我们会有一系列【接口相同、不同实现类 の Bean】。

例如，注册用户时，我们要对【email、password和name】这【3个变量】进行【验证】。

为了【便于扩展】，我们先定义【验证接口】： */
public interface Validator { // Validator n. 验证器
	void validate(String email, String password, String name);
}


/*
然后，分别使用【3个Validator】对【用户参数】进行【验证】： */
@Component
public class PasswordValidator implements Validator {
	public void validate(String email, String password, String name) {
		if (!email.matches("^[a-z0-9]+\\@[a-z0-9]+\\.[a-z]{2,10}$")) {
            throw new IllegalArgumentException("invalid email: " + email);
        }
	}
}

@Component
public class EmailValidator implements Validator {
	public void validate(String email, String password, String name) {
		if (!password.matches("^.{6, 20}$")) {
			throw new IllegalArgumentException("invalid password.");
		}
	}
}

@Component
public class NameValidator implements Validator {
    public void validate(String email, String password, String name) {
        if (name == null || name.isBlank() || name.length() > 20) {
            throw new IllegalArgumentException("invalid name: " + name);
        }
    }
}


/*
最后，我们通过一个Validators作为入口进行验证： */
@Component
public class Validators {
	@Autowired
	List<Validator> validators;

	public void validate(String email, String password, String name) {
		for (var validator : this.validators) {
			validator.vlidate(email, password, name);
		}
	}
}


/*
注意到【Validators】被注入了【一个List<Validator>】，

【Spring】会自动把【所有类型为Validator的Bean】装配为【一个List】注入进来

这样一来，我们【每新增一个Validator类型】，就自动被【Spring】装配到【Validators】中了，非常方便。

因为【Spring】是通过【扫描classpath】获取到【所有的Bean】，而【List】是【有序的】，要指定【List中Bean的顺序】，可以【加上@Order注解】： */
@Componenet
@Order(1)
public class EmailValidator implements Validator {
	// ...
}

@Component
@Order(2)
public class PasswordValidator implements Validator {
	//...
}

@Component
@Order(3)
public class NameValidator implements Validator {
	//...
}

/*========================================================

#	可选注入

默认情况下，当我们标记了一个【@Autowired】后，【Spring】如果没有找到【对应类型的Bean】，它会抛出【NoSuchBeanDefinitionException异常】。

可以给【@Autowired】增加一个【required = false】的参数： */
@Component
public class MailService {
	@Autowired(required = false) 
	ZoneId zoneId = ZoneId.systemDefault();
}

/*
这个参数告诉Spring容器，如果找到一个类型为ZoneId的Bean，就注入，如果找不到，就忽略。

这种方式非常适合有定义就使用定义，没有就使用默认值的情况。

================================

#	--创建第三方Bean

如果【一个Bean】不在【我们自己的package管理】之类，例如【ZoneId】，如何创建它？

答案是我们自己在【@Configuration类】中编写【一个Java方法】创建并返回【它】，注意给【方法】标记一个【@Bean注解】： */

@Configuration 
@ComponentScan
public class AppConfig {

	// 创建一个Bean
	@Bean
	ZoneId createZoneId() {
		return ZoneId.of("Z");
	}
}

/*
【Spring】对【标记为@Bean的方法】只【调用一次】，因此【返回的Bean】仍然是【单例】。


=============================================================


#	初始化和销毁

有些时候，【一个Bean】在注入【必要的依赖】后，需要【进行初始化】（监听消息等）。

在【容器关闭】时，有时候还需要【清理资源】（关闭连接池等）。

我们通常会
	· 定义一个【init()方法】进行【初始化】，
	· 定义一个【shutdown()方法】进行【清理】，

然后，引入【JSR-250定义的Annotation】：
---------------------------------------------

	<dependency>
	    <groupId>javax.annotation</groupId>
	    <artifactId>javax.annotation-api</artifactId>
	    <version>1.3.2</version>
	</dependency>
	
------------------------------------------


在【Bean的初始化】和【清理方法】上标记【@PostConstruct 和 @PreDestroy】： */
@Component
public class MailService {
	@Autowired(required = false) 
	ZoneId zoneId = ZoneId.systemDefault();

	@PostConstruct 
	public void init() {
		System.out.println("Init mail service with zoneId = " + this.zoneId);
	}

	@PreDestroy
	public void shutdown() {
		System.out.println("Shutdown mail service");
	}
}


/*
Spring容器会对上述Bean做如下初始化流程：

· 调用构造方法创建MailService实例；
· 根据@Autowired进行注入；
· 调用标记有@PostConstruct的init()方法进行初始化。

而销毁时，容器会首先调用标记有@PreDestroy的shutdown()方法。

Spring只根据Annotation查找无参数方法，对方法名不作要求。


===========================================================


#	使用别名

默认情况下，对【一种类型的Bean】，【容器】只创建【一个实例】。

但有些时候，我们需要【对一种类型的Bean】创建【多个实例】。

例如，同时连接【多个数据库】，就必须【创建多个DataSource实例】。

如果我们在【@Configuration类】中创建了【多个同类型的Bean】： */
@Configuration
@ComponentScan
public class AppConfig {
	@Bean
	ZoneId createZoneOfZ() {
		return ZoneId.of("Z");
	}

	@Bean
	ZoneId createZoneOfUTC8() {
		return ZoneId.of("UTC+08:00");
	}
}


/*
【Spring】会报【NoUniqueBeanDefinitionException异常】

意思是【出现了重复的Bean定义】。

这个时候，需要【给每个Bean】添加【不同的名字】： */
@Configuration
@ComponentScan
public class AppConfig {
	@Bean("z")
	ZoneId createZoneOfZ() {
		return ZoneId.of("Z");
	}

	@Bean
	@Qualifier("utc8") // Qualifier 修饰符;限定符;限定词;修饰词;限定语
	ZoneId createZoneOfUTC8() {
		return ZoneId.of("UTC+08:00");
	}
}

/*
可以用
@Bean("name") 指定【别名】

也可以用
	@Bean + @Qualifier("name") 指定【别名】。

存在【多个同类型的Bean】时，【注入ZoneId】又会报错： 
	NoUniqueBeanDefinitionException: No qualifying bean of type 'java.time.ZoneId' available: expected single matching bean but found 2


---------------------------

意思是期待找到【唯一的ZoneId类型のBean】，但是找到两。

因此，【注入时】，要指定【Bean的名称】： */
@Component
public class MailService {
	@Autowired(required = false) 
	@Qualifier("z") // 指定ZoneId的"注入名称" = z
	ZoneId zoneId = zoneId.systemDefault();
	//...
}


/*
还有一种方法是把其中某个Bean指定为@Primary： */
@Configuration
@ComponentScan
public class AppConfig {
	@Bean
	@Primary // 指定为"主要的Bean"
	@Qualifier("z")
	ZoneId createZoneOfZ() {
		return ZoneId.of("Z");
	}

	@Bean
	@Qualifier("utc8")
	ZoneId createZoneOfUTC8() {
		return ZoneId.of("UTC+08:00");
	}
}

/*
这样，在【注入时】，如果【没有指出Bean的名字】，【Spring】会注入【标记有@Primary的Bean】。

这种方式也很常用。
例如，对于【主从两个数据源】，通常将【主数据源】定义为【@Primary】： */
@Configuration
@ComponentScan
public class AppConfig {
	@Bean
	@Primary
	DataSource createMasterDataSource() {
		//...
	}

	@Bean
	@Qualifier("slave")
	DataSource createSlaveDataSource() {
		//...
	}
}

/*
【其他Bean】默认注入的就是【主数据源】。

如果要注入【从数据源】，那么【只需要指定"名称"即可】。


===============================================================


#	使用FactoryBean

我们在【设计模式】的【工厂方法】中讲到，很多时候，可以通过【工厂模式】创建对象。

【Spring】也提供了【工厂模式】，允许定义【一个工厂】，然后由【工厂】创建【真正的Bean】。

用【工厂模式】创建Bean需要【实现FactoryBean接口】。

我们观察下面的代码： */
@Component
public class ZoneIdFactoryBean implements FactoryBean<ZoneId> {

	String zone = "Z";

	@Override
	public ZoneId getObject() throws Exception {
		return ZoneId.of(zone);
	}

	@Override
	public Class<?> getObjectType() {
		return ZoneId.class;
	}
}

/*
当【一个Bean】实现了【FactoryBean接口】后，【Spring】会先实例化【这个工厂】，然后【调用FactoryBean<?>.getObject()方法】创建【真正的Bean】。

FactoryBean<?>.getObjectType()可以指定【创建的Bean的类型】，因为【指定类型】不一定与【实际类型】一致，可以是【接口】或【抽象类】。

因此，如果定义了【一个FactoryBean】，要注意【Spring】创建的【Bean】实际上是【这个FactoryBean的getObject()方法】返回的【Bean】。


PS: 为了和【普通Bean】区分，我们通常都以【XxxFactoryBean】命名。


============================================================


#	练习

定制Bean */



/*=============================================================

#	----- 定制Bean の 小结 ----- 


1. 【Spring】默认使用【Singleton】创建【Bean】，也可指定【Scope】为【Prototype】；

	@Component
	@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype")

2. 可将【相同类型的Bean】注入【List】；
	因为List是有顺序, 可通过附加 @Order(number) 指定 第number个放入List中

	@Order(num)
	@Autowired  <-- 自动被Spring装配

3. 可用@Autowired(required=false)允许【可选注入】；

	@Autowired(required = false) : 
		如果找到一个【类型】为【被注解的Class】的"Bean"，就注入; 如果找不到，就忽略使用Bean, 直接运行下述代码。

4. 可用带【@Bean标注的方法】创建【Bean】;
	在【注解@Configuration】的Class中, 写入 @Bean + 编写一个Java方法 引入【第三方Bean】

	@Bean

5. 可使用
	@PostConstruct 对 Bean 进行【初始化】
	@PreDestroy 对 Bean 进行【清理】；

6. 【相同类型的Bean】只能有一个Bean【指定为@Primary】，其他必须用【@Quanlifier("beanName")】指定【别名】；

	@Primary 	// 指定为主要Bean

7. 注入时，可通过 @Bean + @Quanlifier("beanName") 指定 【某个Bean】；
	
	@Bean("bean_name")		<-- 设定Bean的名字

	@Qualifier("bean_name") <-- 指定注入的Bean的名字

8. 可以定义【FactoryBean】来使用【工厂模式】创建【Bean】。 
	
	class Xxx implements FactoryBean<Method_return_Class> { .. }
*/
@Component
public class ZoneIdFactoryBean implements FactoryBean<ZoneId> {

	String zone = "Z";

    @Override
    public ZoneId getObject() throws Exception {
        return ZoneId.of(zone);
    }

    @Override
    public Class<?> getObjectType() {
        return ZoneId.class;
    }
}



































